﻿namespace JsViewEngine
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Text;
    using System.Web;

    internal class JsTemplateManager
    {
        private readonly HttpContextBase context;

        private static JsTemplateManager instance;

        public static JsTemplateManager GetInstance(HttpContextBase context)
        {
            return instance ?? (instance = new JsTemplateManager(context));
        }

        public JsTemplateManager(HttpContextBase context)
        {
            this.context = context;

            this.TemplateLocationFormats = new[] { "~/Views/{1}/{0}.jsrender", "~/Views/Shared/{0}.jsrender" };
        }

        /// <summary>
        /// Gets or sets the partial-view location formats.
        /// </summary>
        /// <remarks>
        /// {0} is substituted with the template name, {1} is substituted with the controller name.
        /// </remarks>
        public string[] TemplateLocationFormats { get; set; }

        public IHtmlString Render(params string[] names)
        {
            var stringBuilder = new StringBuilder();

            string controllerName = this.context.Request.RequestContext.RouteData.GetRequiredString("controller");
            foreach (var name in names)
            {
                var path = this.GetTemplatePath(name, controllerName);
                string rawContents = File.ReadAllText(path);

                stringBuilder.Append(RenderScriptOpenTag(name));
                stringBuilder.Append(Environment.NewLine);
                stringBuilder.Append(rawContents);
                stringBuilder.Append(RenderScriptCloseTag());
                stringBuilder.Append(Environment.NewLine);
            }

            return new HtmlString(stringBuilder.ToString());
        }

        public IHtmlString RenderJsString(string name)
        {
            string controllerName = this.context.Request.RequestContext.RouteData.GetRequiredString("controller");
            var path = this.GetTemplatePath(name, controllerName);
            string rawContents = File.ReadAllText(path);
            string escaped = ToEscapedJavaScriptString(rawContents, '\"', false);

            return new HtmlString(escaped);
        }

        private string GetTemplatePath(string name, string controllerName)
        {
            var locationsSearched = new List<string>();
            string sourcePath;

            if (name.StartsWith("~"))
            {
                // it's an absolute path
                sourcePath = this.context.Server.MapPath(name);
                if (File.Exists(sourcePath))
                {
                    return sourcePath;
                }
                
                throw new InvalidOperationException(
                    string.Format("View not found at absolute path: \"{0}\"", sourcePath));
            }
            
            foreach (var locationFormat in this.TemplateLocationFormats)
            {
                var location = string.Format(locationFormat, name, controllerName);
                sourcePath = this.context.Server.MapPath(location);

                if (File.Exists(sourcePath))
                {
                    return sourcePath;
                }

                locationsSearched.Add(sourcePath);
            }

            throw new InvalidOperationException(
                string.Format("View not found in following locations \"{0}\"", string.Join("\", \"", locationsSearched)));
        }

        private static string RenderScriptCloseTag()
        {
            return "</script>" + Environment.NewLine;
        }

        private static string RenderScriptOpenTag(string name)
        {
            return string.Format(
                "<script id=\"{0}\" type=\"text/x-jsrender\">{1}", HttpUtility.HtmlEncode(name), Environment.NewLine);
        }

        private static string ToEscapedJavaScriptString(string value, char delimiter, bool appendDelimiters)
        {
            int capacity = value != null ? value.Length : 16;
            using (var stringWriter = new StringWriter(new StringBuilder(capacity), CultureInfo.InvariantCulture))
            {
                WriteEscapedJavaScriptString(stringWriter, value, delimiter, appendDelimiters);
                return stringWriter.ToString();
            }
        }

        private static void WriteEscapedJavaScriptString(TextWriter writer, string s, char delimiter, bool appendDelimiters)
        {
            if (appendDelimiters)
            {
                writer.Write(delimiter);
            }

            if (s != null)
            {
                char[] buffer = null;
                int index1 = 0;
                for (int index2 = 0; index2 < s.Length; ++index2)
                {
                    char c = s[index2];
                    if (c < 32 || c >= 128 || (c == 92 || c == delimiter))
                    {
                        string str;
                        switch (c)
                        {
                            case '\\':
                                str = "\\\\";
                                break;
                            case '\x0085':
                                str = "\\u0085";
                                break;
                            case '\x2028':
                                str = "\\u2028";
                                break;
                            case '\x2029':
                                str = "\\u2029";
                                break;
                            case '\b':
                                str = "\\b";
                                break;
                            case '\t':
                                str = "\\t";
                                break;
                            case '\n':
                                str = "\\n";
                                break;
                            case '\f':
                                str = "\\f";
                                break;
                            case '\r':
                                str = "\\r";
                                break;
                            case '"':
                                str = "\\\"";
                                break;
                            case '\'':
                                str = "\\'";
                                break;
                            default:
                                str = (int)c <= 31 ? ToCharAsUnicode(c) : null;
                                break;
                        }
                        if (str != null)
                        {
                            if (index2 > index1)
                            {
                                if (buffer == null)
                                    buffer = s.ToCharArray();
                                writer.Write(buffer, index1, index2 - index1);
                            }
                            index1 = index2 + 1;
                            writer.Write(str);
                        }
                    }
                }

                if (index1 == 0)
                {
                    writer.Write(s);
                }
                else
                {
                    if (buffer == null)
                        buffer = s.ToCharArray();
                    writer.Write(buffer, index1, s.Length - index1);
                }
            }

            if (!appendDelimiters)
            {
                return;
            }

            writer.Write(delimiter);
        }

        private static string ToCharAsUnicode(char c)
        {
            return new string(
                    new char[6]
                        {
                            '\\', 'u', 
                            IntToHex(c >> 12 & 15), IntToHex(c >> 8 & 15), 
                            IntToHex(c >> 4 & 15), IntToHex(c & 15)
                        });
        }

        private static char IntToHex(int n)
        {
            if (n <= 9)
            {
                return (char)(n + 48);
            }
            
            return (char)(n - 10 + 97);
        }
    }
}